/*
 * Copyright 2012-2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.cloud.client.serviceregistry;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import jakarta.annotation.PreDestroy;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.beans.BeansException;
import org.springframework.boot.web.context.ConfigurableWebServerApplicationContext;
import org.springframework.boot.web.context.WebServerInitializedEvent;
import org.springframework.cloud.client.discovery.ManagementServerPortUtils;
import org.springframework.cloud.client.discovery.event.InstancePreRegisteredEvent;
import org.springframework.cloud.client.discovery.event.InstanceRegisteredEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.core.env.Environment;

/**
 * Lifecycle methods that may be useful and common to {@link ServiceRegistry}
 * implementations.
 * <p>
 * TODO: Document the lifecycle.
 *
 * @param <R> Registration type passed to the {@link ServiceRegistry}.
 * @author Spencer Gibb
 * @author Zen Huifer
 */
public abstract class AbstractAutoServiceRegistration<R extends Registration>
        implements AutoServiceRegistration, ApplicationContextAware, ApplicationListener<WebServerInitializedEvent> {

    private static final Log logger = LogFactory.getLog(AbstractAutoServiceRegistration.class);

    private final ServiceRegistry<R> serviceRegistry;

    private final boolean autoStartup = true;

    private final AtomicBoolean running = new AtomicBoolean(false);

    private final int order = 0;

    private final AtomicInteger port = new AtomicInteger(0);

    private ApplicationContext context;

    private Environment environment;

    private AutoServiceRegistrationProperties properties;

    private List<RegistrationManagementLifecycle<R>> registrationManagementLifecycles = new ArrayList<>();

    private List<RegistrationLifecycle<R>> registrationLifecycles = new ArrayList<>();

    protected AbstractAutoServiceRegistration(ServiceRegistry<R> serviceRegistry,
                                              AutoServiceRegistrationProperties properties) {
        this.serviceRegistry = serviceRegistry;
        this.properties = properties;
    }

    protected AbstractAutoServiceRegistration(ServiceRegistry<R> serviceRegistry,
                                              AutoServiceRegistrationProperties properties,
                                              List<RegistrationManagementLifecycle<R>> registrationManagementLifecycles,
                                              List<RegistrationLifecycle<R>> registrationLifecycles) {
        this.serviceRegistry = serviceRegistry;
        this.properties = properties;
        this.registrationManagementLifecycles = registrationManagementLifecycles;
        this.registrationLifecycles = registrationLifecycles;
    }

    protected AbstractAutoServiceRegistration(ServiceRegistry<R> serviceRegistry,
                                              AutoServiceRegistrationProperties properties,
                                              List<RegistrationLifecycle<R>> registrationLifecycles) {
        this.serviceRegistry = serviceRegistry;
        this.properties = properties;
        this.registrationLifecycles = registrationLifecycles;
    }

    public void addRegistrationManagementLifecycle(RegistrationManagementLifecycle<R> registrationManagementLifecycle) {
        this.registrationManagementLifecycles.add(registrationManagementLifecycle);
    }

    public void addRegistrationLifecycle(RegistrationLifecycle<R> registrationLifecycle) {
        this.registrationLifecycles.add(registrationLifecycle);
    }

    protected ApplicationContext getContext() {
        return this.context;
    }

    @Override
    @SuppressWarnings("deprecation")
    public void onApplicationEvent(WebServerInitializedEvent event) {
        ApplicationContext context = event.getApplicationContext();
        if (context instanceof ConfigurableWebServerApplicationContext) {
            if ("management".equals(((ConfigurableWebServerApplicationContext) context).getServerNamespace())) {
                return;
            }
        }
        this.port.compareAndSet(0, event.getWebServer().getPort());
        this.start();
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.context = applicationContext;
        this.environment = this.context.getEnvironment();
    }

    @Deprecated
    protected Environment getEnvironment() {
        return this.environment;
    }

    @Deprecated
    protected AtomicInteger getPort() {
        return this.port;
    }

    public boolean isAutoStartup() {
        return this.autoStartup;
    }

    public void start() {
        if (!isEnabled()) {
            if (logger.isDebugEnabled()) {
                logger.debug("Discovery Lifecycle disabled. Not starting");
            }
            return;
        }

        // only initialize if nonSecurePort is greater than 0 and it isn't already running
        // because of containerPortInitializer below
        if (!this.running.get()) {
            this.context.publishEvent(new InstancePreRegisteredEvent(this, getRegistration()));
            registrationLifecycles.forEach(registrationLifecycle -> registrationLifecycle.postProcessBeforeStartRegister(getRegistration()));
            register();
            this.registrationLifecycles.forEach(registrationLifecycle -> registrationLifecycle.postProcessAfterStartRegister(getRegistration()));
            if (shouldRegisterManagement()) {
                this.registrationManagementLifecycles.forEach(registrationManagementLifecycle -> registrationManagementLifecycle.postProcessBeforeStartRegisterManagement(getManagementRegistration()));
                this.registerManagement();
                registrationManagementLifecycles.forEach(registrationManagementLifecycle -> registrationManagementLifecycle.postProcessAfterStartRegisterManagement(getManagementRegistration()));

            }
            this.context.publishEvent(new InstanceRegisteredEvent<>(this, getConfiguration()));
            this.running.compareAndSet(false, true);
        }

    }

    /**
     * @return Whether the management service should be registered with the
     * {@link ServiceRegistry}.
     */
    protected boolean shouldRegisterManagement() {
        if (this.properties == null || this.properties.isRegisterManagement()) {
            return getManagementPort() != null && ManagementServerPortUtils.isDifferent(this.context);
        }
        return false;
    }

    /**
     * @return The object used to configure the registration.
     */
    @Deprecated
    protected abstract Object getConfiguration();

    /**
     * @return True, if this is enabled.
     */
    protected abstract boolean isEnabled();

    /**
     * @return The serviceId of the Management Service.
     */
    @Deprecated
    protected String getManagementServiceId() {
        // TODO: configurable management suffix
        return this.context.getId() + ":management";
    }

    /**
     * @return The service name of the Management Service.
     */
    @Deprecated
    protected String getManagementServiceName() {
        // TODO: configurable management suffix
        return getAppName() + ":management";
    }

    /**
     * @return The management server port.
     */
    @Deprecated
    protected Integer getManagementPort() {
        return ManagementServerPortUtils.getPort(this.context);
    }

    /**
     * @return The app name (currently the spring.application.name property).
     */
    @Deprecated
    protected String getAppName() {
        return this.environment.getProperty("spring.application.name", "application");
    }

    @PreDestroy
    public void destroy() {
        stop();
    }

    public boolean isRunning() {
        return this.running.get();
    }

    protected AtomicBoolean getRunning() {
        return this.running;
    }

    public int getOrder() {
        return this.order;
    }

    public int getPhase() {
        return 0;
    }

    protected ServiceRegistry<R> getServiceRegistry() {
        return this.serviceRegistry;
    }

    protected abstract R getRegistration();

    protected abstract R getManagementRegistration();

    /**
     * Register the local service with the {@link ServiceRegistry}.
     */
    protected void register() {
        this.serviceRegistry.register(getRegistration());
    }

    /**
     * Register the local management service with the {@link ServiceRegistry}.
     */
    protected void registerManagement() {
        R registration = getManagementRegistration();
        if (registration != null) {
            this.serviceRegistry.register(registration);
        }
    }

    /**
     * De-register the local service with the {@link ServiceRegistry}.
     */
    protected void deregister() {
        this.serviceRegistry.deregister(getRegistration());
    }

    /**
     * De-register the local management service with the {@link ServiceRegistry}.
     */
    protected void deregisterManagement() {
        R registration = getManagementRegistration();
        if (registration != null) {
            this.serviceRegistry.deregister(registration);
        }
    }

    public void stop() {
        if (this.getRunning().compareAndSet(true, false) && isEnabled()) {

            this.registrationLifecycles.forEach(registrationLifecycle -> registrationLifecycle.postProcessBeforeStopRegister(getRegistration()));
            deregister();
            this.registrationLifecycles.forEach(registrationLifecycle -> registrationLifecycle.postProcessAfterStopRegister(getRegistration()));
            if (shouldRegisterManagement()) {
                this.registrationManagementLifecycles.forEach(registrationManagementLifecycle -> registrationManagementLifecycle.postProcessBeforeStopRegisterManagement(getManagementRegistration()));
                deregisterManagement();
                this.registrationManagementLifecycles.forEach(registrationManagementLifecycle -> registrationManagementLifecycle.postProcessAfterStopRegisterManagement(getManagementRegistration()));
            }
            this.serviceRegistry.close();
        }
    }

}
